Upgrade python/httplib2 to v0.17.0 Test: None Change-Id: I4189cf870ba80629316fbe58c56b996fa0a082d5 
diff --git a/CHANGELOG b/CHANGELOG index 07fb949..1dee4c4 100644 --- a/CHANGELOG +++ b/CHANGELOG 
@@ -1,3 +1,18 @@ +0.17.0 + + feature: Http().redirect_codes set, works after follow(_all)_redirects check + This allows one line workaround for old gcloud library that uses 308 + response without redirect semantics. + https://github.com/httplib2/httplib2/issues/156 + +0.16.0 + + IMPORTANT cache invalidation change, fix 307 keep method, add 308 Redirects + https://github.com/httplib2/httplib2/issues/151 + + proxy: username/password as str compatible with pysocks + https://github.com/httplib2/httplib2/issues/154 +  0.15.0    python2: regression in connect() error handling 
diff --git a/METADATA b/METADATA index 8b3c1c6..605407f 100644 --- a/METADATA +++ b/METADATA 
@@ -9,10 +9,10 @@  type: GIT  value: "https://github.com/httplib2/httplib2.git"  } - version: "v0.15.0" + version: "v0.17.0"  last_upgrade_date { - year: 2019 - month: 12 - day: 23 + year: 2020 + month: 2 + day: 1  }  } 
diff --git a/python2/httplib2/__init__.py b/python2/httplib2/__init__.py index c8302eb..f32accf 100644 --- a/python2/httplib2/__init__.py +++ b/python2/httplib2/__init__.py 
@@ -19,7 +19,7 @@  "Alex Yu",  ]  __license__ = "MIT" -__version__ = '0.15.0' +__version__ = '0.17.0'    import base64  import calendar @@ -291,6 +291,12 @@  "upgrade",  ]   +# https://tools.ietf.org/html/rfc7231#section-8.1.3 +SAFE_METHODS = ("GET", "HEAD") # TODO add "OPTIONS", "TRACE" + +# To change, assign to `Http().redirect_codes` +REDIRECT_CODES = frozenset((300, 301, 302, 303, 307, 308)) +    def _get_end2end_headers(response):  hopbyhop = list(HOP_BY_HOP) @@ -1175,9 +1181,9 @@    host = self.host  port = self.port -  +  socket_err = None -  +  for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):  af, socktype, proto, canonname, sa = res  try: @@ -1353,9 +1359,9 @@    host = self.host  port = self.port -  +  socket_err = None -  +  address_info = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)  for family, socktype, proto, canonname, sockaddr in address_info:  try: @@ -1661,10 +1667,14 @@  # If set to False then no redirects are followed, even safe ones.  self.follow_redirects = True   + self.redirect_codes = REDIRECT_CODES +  # Which HTTP methods do we apply optimistic concurrency to, i.e.  # which methods get an "if-match:" etag header added to them.  self.optimistic_concurrency_methods = ["PUT", "PATCH"]   + self.safe_methods = list(SAFE_METHODS) +  # If 'follow_redirects' is True, and this is set to True then  # all redirecs are followed, including unsafe ones.  self.follow_all_redirects = False @@ -1858,10 +1868,10 @@    if (  self.follow_all_redirects - or (method in ["GET", "HEAD"]) - or response.status == 303 + or method in self.safe_methods + or response.status in (303, 308)  ): - if self.follow_redirects and response.status in [300, 301, 302, 303, 307]: + if self.follow_redirects and response.status in self.redirect_codes:  # Pick out the location header and basically start from the beginning  # remembering first to strip the ETag header and decrement our 'depth'  if redirections: @@ -1881,7 +1891,7 @@  response["location"] = urlparse.urljoin(  absolute_uri, location  ) - if response.status == 301 and method in ["GET", "HEAD"]: + if response.status == 308 or (response.status == 301 and method in self.safe_methods):  response["-x-permanent-redirect-url"] = response["location"]  if "content-location" not in response:  response["content-location"] = absolute_uri @@ -1918,7 +1928,7 @@  response,  content,  ) - elif response.status in [200, 203] and method in ["GET", "HEAD"]: + elif response.status in [200, 203] and method in self.safe_methods:  # Don't cache 206's since we aren't going to handle byte range requests  if "content-location" not in response:  response["content-location"] = absolute_uri @@ -2018,6 +2028,7 @@  headers["accept-encoding"] = "gzip, deflate"    info = email.Message.Message() + cachekey = None  cached_value = None  if self.cache:  cachekey = defrag_uri.encode("utf-8") @@ -2038,8 +2049,6 @@  self.cache.delete(cachekey)  cachekey = None  cached_value = None - else: - cachekey = None    if (  method in self.optimistic_concurrency_methods @@ -2051,13 +2060,15 @@  # http://www.w3.org/1999/04/Editing/  headers["if-match"] = info["etag"]   - if method not in ["GET", "HEAD"] and self.cache and cachekey: - # RFC 2616 Section 13.10 + # https://tools.ietf.org/html/rfc7234 + # A cache MUST invalidate the effective Request URI as well as [...] Location and Content-Location + # when a non-error status code is received in response to an unsafe request method. + if self.cache and cachekey and method not in self.safe_methods:  self.cache.delete(cachekey)    # Check the vary header in the cache to see if this request  # matches what varies in the cache. - if method in ["GET", "HEAD"] and "vary" in info: + if method in self.safe_methods and "vary" in info:  vary = info["vary"]  vary_headers = vary.lower().replace(" ", "").split(",")  for header in vary_headers: @@ -2068,11 +2079,14 @@  break    if ( - cached_value - and method in ["GET", "HEAD"] - and self.cache + self.cache + and cached_value + and (method in self.safe_methods or info["status"] == "308")  and "range" not in headers  ): + redirect_method = method + if info["status"] not in ("307", "308"): + redirect_method = "GET"  if "-x-permanent-redirect-url" in info:  # Should cached permanent redirects be counted in our redirection count? For now, yes.  if redirections <= 0: @@ -2083,7 +2097,7 @@  )  (response, new_content) = self.request(  info["-x-permanent-redirect-url"], - method="GET", + method=redirect_method,  headers=headers,  redirections=redirections - 1,  ) 
diff --git a/python2/httplib2/socks.py b/python2/httplib2/socks.py index 5cef776..71eb4eb 100644 --- a/python2/httplib2/socks.py +++ b/python2/httplib2/socks.py 
@@ -238,7 +238,15 @@  headers - Additional or modified headers for the proxy connect  request.  """ - self.__proxy = (proxytype, addr, port, rdns, username, password, headers) + self.__proxy = ( + proxytype, + addr, + port, + rdns, + username.encode() if username else None, + password.encode() if password else None, + headers, + )    def __negotiatesocks5(self, destaddr, destport):  """__negotiatesocks5(self,destaddr,destport) 
diff --git a/python3/httplib2/__init__.py b/python3/httplib2/__init__.py index d8c3d34..6467c79 100644 --- a/python3/httplib2/__init__.py +++ b/python3/httplib2/__init__.py 
@@ -15,7 +15,7 @@  "Alex Yu",  ]  __license__ = "MIT" -__version__ = '0.15.0' +__version__ = '0.17.0'    import base64  import calendar @@ -161,6 +161,13 @@  "upgrade",  ]   +# https://tools.ietf.org/html/rfc7231#section-8.1.3 +SAFE_METHODS = ("GET", "HEAD", "OPTIONS", "TRACE") + +# To change, assign to `Http().redirect_codes` +REDIRECT_CODES = frozenset((300, 301, 302, 303, 307, 308)) + +  from httplib2 import certs  CA_CERTS = certs.where()   @@ -315,7 +322,7 @@  # Whether to use a strict mode to parse WWW-Authenticate headers  # Might lead to bad results in case of ill-formed header value,  # so disabled by default, falling back to relaxed parsing. -# Set to true to turn on, usefull for testing servers. +# Set to true to turn on, useful for testing servers.  USE_WWW_AUTH_STRICT_PARSING = 0    # In regex below: @@ -1004,10 +1011,10 @@  proxy_headers: Additional or modified headers for the proxy connect  request.  """ - if isinstance(proxy_user, str): - proxy_user = proxy_user.encode() - if isinstance(proxy_pass, str): - proxy_pass = proxy_pass.encode() + if isinstance(proxy_user, bytes): + proxy_user = proxy_user.decode() + if isinstance(proxy_pass, bytes): + proxy_pass = proxy_pass.decode()  self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, self.proxy_user, self.proxy_pass, self.proxy_headers = (  proxy_type,  proxy_host, @@ -1467,10 +1474,14 @@  # If set to False then no redirects are followed, even safe ones.  self.follow_redirects = True   + self.redirect_codes = REDIRECT_CODES +  # Which HTTP methods do we apply optimistic concurrency to, i.e.  # which methods get an "if-match:" etag header added to them.  self.optimistic_concurrency_methods = ["PUT", "PATCH"]   + self.safe_methods = list(SAFE_METHODS) +  # If 'follow_redirects' is True, and this is set to True then  # all redirecs are followed, including unsafe ones.  self.follow_all_redirects = False @@ -1663,10 +1674,10 @@    if (  self.follow_all_redirects - or (method in ["GET", "HEAD"]) - or response.status == 303 + or method in self.safe_methods + or response.status in (303, 308)  ): - if self.follow_redirects and response.status in [300, 301, 302, 303, 307]: + if self.follow_redirects and response.status in self.redirect_codes:  # Pick out the location header and basically start from the beginning  # remembering first to strip the ETag header and decrement our 'depth'  if redirections: @@ -1686,7 +1697,7 @@  response["location"] = urllib.parse.urljoin(  absolute_uri, location  ) - if response.status == 301 and method in ["GET", "HEAD"]: + if response.status == 308 or (response.status == 301 and (method in self.safe_methods)):  response["-x-permanent-redirect-url"] = response["location"]  if "content-location" not in response:  response["content-location"] = absolute_uri @@ -1723,7 +1734,7 @@  response,  content,  ) - elif response.status in [200, 203] and method in ["GET", "HEAD"]: + elif response.status in [200, 203] and method in self.safe_methods:  # Don't cache 206's since we aren't going to handle byte range requests  if "content-location" not in response:  response["content-location"] = absolute_uri @@ -1822,6 +1833,7 @@  headers["accept-encoding"] = "gzip, deflate"    info = email.message.Message() + cachekey = None  cached_value = None  if self.cache:  cachekey = defrag_uri @@ -1839,8 +1851,6 @@  self.cache.delete(cachekey)  cachekey = None  cached_value = None - else: - cachekey = None    if (  method in self.optimistic_concurrency_methods @@ -1852,13 +1862,15 @@  # http://www.w3.org/1999/04/Editing/  headers["if-match"] = info["etag"]   - if method not in ["GET", "HEAD"] and self.cache and cachekey: - # RFC 2616 Section 13.10 + # https://tools.ietf.org/html/rfc7234 + # A cache MUST invalidate the effective Request URI as well as [...] Location and Content-Location + # when a non-error status code is received in response to an unsafe request method. + if self.cache and cachekey and method not in self.safe_methods:  self.cache.delete(cachekey)    # Check the vary header in the cache to see if this request  # matches what varies in the cache. - if method in ["GET", "HEAD"] and "vary" in info: + if method in self.safe_methods and "vary" in info:  vary = info["vary"]  vary_headers = vary.lower().replace(" ", "").split(",")  for header in vary_headers: @@ -1869,11 +1881,14 @@  break    if ( - cached_value - and method in ["GET", "HEAD"] - and self.cache + self.cache + and cached_value + and (method in self.safe_methods or info["status"] == "308")  and "range" not in headers  ): + redirect_method = method + if info["status"] not in ("307", "308"): + redirect_method = "GET"  if "-x-permanent-redirect-url" in info:  # Should cached permanent redirects be counted in our redirection count? For now, yes.  if redirections <= 0: @@ -1884,7 +1899,7 @@  )  (response, new_content) = self.request(  info["-x-permanent-redirect-url"], - method="GET", + method=redirect_method,  headers=headers,  redirections=redirections - 1,  ) 
diff --git a/python3/httplib2/socks.py b/python3/httplib2/socks.py index 2926b4e..cc68e63 100644 --- a/python3/httplib2/socks.py +++ b/python3/httplib2/socks.py 
@@ -238,7 +238,15 @@  headers - Additional or modified headers for the proxy connect  request.  """ - self.__proxy = (proxytype, addr, port, rdns, username, password, headers) + self.__proxy = ( + proxytype, + addr, + port, + rdns, + username.encode() if username else None, + password.encode() if password else None, + headers, + )    def __negotiatesocks5(self, destaddr, destport):  """__negotiatesocks5(self,destaddr,destport) 
diff --git a/setup.py b/setup.py index 33c8827..a3be8d4 100755 --- a/setup.py +++ b/setup.py 
@@ -4,7 +4,7 @@  import sys    pkgdir = {"": "python%s" % sys.version_info[0]} -VERSION = '0.15.0' +VERSION = '0.17.0'      # `python setup.py test` uses existing Python environment, no virtualenv, no pip. 
diff --git a/tests/__init__.py b/tests/__init__.py index 496652b..a15db9e 100644 --- a/tests/__init__.py +++ b/tests/__init__.py 
@@ -406,10 +406,12 @@  request = HttpRequest.from_buffered(buf)  if request is None:  break + # print("--- debug request\n" + request.raw.decode("ascii", "replace"))  i += 1  request.client_sock = sock  request.number = i  response = request_handler(request=request) + # print("--- debug response\n" + response.decode("ascii", "replace"))  sock.sendall(response)  request.client_sock = None  if not tick(request): 
diff --git a/tests/test_http.py b/tests/test_http.py index 97b52dc..df99016 100644 --- a/tests/test_http.py +++ b/tests/test_http.py 
@@ -622,6 +622,57 @@  assert content == b"final content\n"     +def test_post_307(): + # 307: follow with same method + http = httplib2.Http(cache=tests.get_cache_path(), timeout=1) + http.follow_all_redirects = True + r307 = tests.http_response_bytes(status=307, headers={"location": "/final"}) + r200 = tests.http_response_bytes(status=200, body=b"final content\n") + + with tests.server_list_http([r307, r200, r307, r200]) as uri: + response, content = http.request(uri, "POST") + assert response.previous.status == 307 + assert not response.previous.fromcache + assert response.status == 200 + assert not response.fromcache + assert content == b"final content\n" + + response, content = http.request(uri, "POST") + assert response.previous.status == 307 + assert not response.previous.fromcache + assert response.status == 200 + assert not response.fromcache + assert content == b"final content\n" + + +def test_change_308(): + # 308: follow with same method, cache redirect + http = httplib2.Http(cache=tests.get_cache_path(), timeout=1) + routes = { + "/final": tests.make_http_reflect(), + "": tests.http_response_bytes( + status="308 Permanent Redirect", + add_date=True, + headers={"cache-control": "max-age=300", "location": "/final"}, + ), + } + + with tests.server_route(routes, request_count=3) as uri: + response, content = http.request(uri, "CHANGE", body=b"hello308") + assert response.previous.status == 308 + assert not response.previous.fromcache + assert response.status == 200 + assert not response.fromcache + assert content.startswith(b"CHANGE /final HTTP") + + response, content = http.request(uri, "CHANGE") + assert response.previous.status == 308 + assert response.previous.fromcache + assert response.status == 200 + assert not response.fromcache + assert content.startswith(b"CHANGE /final HTTP") + +  def test_get_410():  # Test that we pass 410's through  http = httplib2.Http() @@ -643,3 +694,12 @@  assert response.status == 200  assert content == b"content"  assert response["link"], "link1, link2" + + +def test_custom_redirect_codes(): + http = httplib2.Http() + http.redirect_codes = set([300]) + with tests.server_const_http(status=301, request_count=1) as uri: + response, content = http.request(uri, "GET") + assert response.status == 301 + assert response.previous is None 
diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 375367f..4ec8aea 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py 
@@ -32,8 +32,8 @@  pi = httplib2.proxy_info_from_url("http://zoidberg:fish@someproxy:99")  assert pi.proxy_host == "someproxy"  assert pi.proxy_port == 99 - assert pi.proxy_user == b"zoidberg" - assert pi.proxy_pass == b"fish" + assert pi.proxy_user == "zoidberg" + assert pi.proxy_pass == "fish"      def test_from_env():